Bugs in Jira API regarding updating of timetracking estimates

Context:
We have a selfmade Jira plugin that does some trickery regarding remaining estimates and subtasks.
Basically it is a listener for IssueEvent and IssuePreDeleteEvent.

  • it sets original and remaining estimates of subtasks to 0 if someone accidentally sets those values
  • it increases or decreases the remaining estimate of the parent issue if work is logged, changed or deleted on a subtask or a subtask is deleted entirely
  • it sets remaining estimate to 0 if any IssueEvent for a parent issue is dispatched and the parent issue has a resolution

This used to work fine in a former version of Jira, but I cannot say in which version it last worked.
We found, that in the version 7.13.8 we currently use, the plugin does not work cleanly and I found that the behavior is pretty strange regarding updating the timetracking estimates in mutliple extends and as far as I can tell this also did not change in Jira 8.

As this can be considered a system action and not a user action, I’m not using the IssueService which does checks on whether the fields are on the current screen, whether the user has the right to change it and so on, but the IssueManager which allows to change the values regardless. Also IssueService does not allow to update the timetracking information once the issue is in closed state, but if the issue is directly closed, the remaining estimate should also be set to 0, so using IssueService is not really an option, besides that it also does not work properly.

The code that should update the value is basically like this:

MutableIssue mutableIssue = issueManager.getIssueObject(issueEvent.getIssue().getId());
mutableIssue.setEstimate(0L); // this for example for setting to 0 for resolved issues
mutableIssue.setEstimate(null); // this and the next for example for forcing the subtask estimates to unset
mutableIssue.setOriginalEstimate(0L);
issueManager.updateIssue(issueEvent.getUser(), mutableIssue, UpdateIssueRequest.builder().build());

The first issue I found was, that IssueImpl which is the type of the object returned by getIssueObject stores the ModifiedValue for both fields in the modifiedFields map under the key "timetracking". That means, if the estimates are currently 0 for original and 5d for remaining, what happens is the following:

  • setEstimate sets the estimate
  • a ModifiedValue for the change 5d -> 0 is put to modifiedFields at key "timetracking"
  • setOriginalEstimate sets the original estimate
  • a ModifiedValue for the change 0 -> 0 is put to modifiedFields at key "timetracking" and thus overwrites the previous ModifiedValue
  • the further processing checks the modifiedFields for actual changes and filters out no-ops like the one currently at key "timetracking"
  • as now modifiedFields is empty, no update is done at all as the logic thinks there was no change which is wrong

While this is wrong and pretty confusing behavior, it was easy to work-around. I added two if statements that only call the respective method if the current value is not null or 0, then the updates worked fine again in that case.

The next issue I found (and I think it is strongly related to the last issue) was, that TimeTrackingSystemField#updateValue which is called by DefaultIssueManager#updateFieldValues which is called by DefaultIssueManager#updateIssue - the method I call from my code - looks for ModifiedValue#getNewValue and checks its type. If the type is not TimeTrackingSystemField.TimeTrackingValue, it treats the the change for the history as being in legacy mode instead of modern mode, which is not the case for our configuration. The problem here seems to be that IssueImpl creates ModifiedValue instances with Long values, regardless whether legacy mode or modern mode is used. The effect of this is, that the history always shows both values to be modified, according to the last ModifiedValue put to modifiedFields. So if you have currently original estimate 1d and remaining estimate 2d and then use above describe method to set remaining estimate to 3d and original estimate to 4d, then the result is correct, but the history states that both estimates were changed from 1d to 4d as the ModifiedValue in modifiedFields is considered legacy mode and so meant for both fields. This only affects the history as far as I can tell. But I guess the correct fix would be to create a ModifiedValue with TimeTrackingSystemField.TimeTrackingValue as value in IssueImpl if modern mode is used. This - if done properly - would probably also fix the last issue, as then the original and remaining estimates can be combined in the one "timetracking" field in modifiedFields instead of one overwriting the other.
I have no work-around for this one, other than either using the IssueService instead of the IssueManager which properly uses the modern mode but is not really an option as detailed above, or depending on jira-core and manually adjusting the modifiedValues with TimeTrackingValue values instead of Longs. Actually that is what I’m doing now, I use the IssueService if validateUpdate was successful and if not, then I use the IssueManager and manually correct the modifiedFields so that IssueImpl then behaves properly.

The most notable and critical issue for me though is, that the updating of the estimates does not work while processing an IssueEvent happening due to a workflow transition. No matter whether I use the IssueService or the IssueManager, if I’m inside my listener triggered by a workflow transition like the issue being resolved or closed, the change gets written to the history with the quirks described above, but the values are not updated but remain like they are. For other IssueEvents like changing the assignee while the issue has a resolution or writing a comment while the issue has a resolution or changing the estimates while the issue has a resolution, the updating of the estimates works fine and the remaining estimate is set to 0 as expected.
I found a work-around for this too by listening for IssueChangedEvent additionally and doing the remaining estimate setting to 0 for resolved issues there too, as from that handler it works as expected with the quirks described above.

So these are basically three issues I’m reporting for which I have more or less work-arounds. But maybe you can tell me a better way to update the estimate values that does not suffer from the three problems described above.

Further, interestingly the wired test didn’t find the last of the problems described above, while a jira-testkit test that does the exact same actions, just via testkit instead of plugin Java API did find the problem, so the wired test somehow got stale or not-fully persisted information which might actually be a fourth problem.

The wired test basically does it like:

MutableIssue parentIssue = issueManager.getIssueObject(parentKey);
parentIssue.setResolutionObject(resolutionManager.getDefaultResolution());
issueManager.updateIssue(adminUser, parentIssue, UpdateIssueRequest.builder().build());
parentIssue = issueManager.getIssueObject(parentKey);
assert parentIssue.getEstimate() == 0

while the testkit test does it like:

IssuesControl issues = new Backdoor(new TestKitLocalEnvironmentData()).issues().loginAs("tester");
issues.transitionIssue(parentKey, transitionId);
assertNotNull("resolution is null", issues.getIssue(parentKey).fields.resolution);
assert Optional.ofNullable(issues.getIssue(parentKey).fields.timetracking).map(tt -> tt.remainingEstimateSeconds).orElse(0L).longValue() == 0