From 1d4cd582136ae7cc3485a639f87e4c85f2285df7 Mon Sep 17 00:00:00 2001 From: marko-bekhta Date: Mon, 18 Nov 2024 22:24:41 +0100 Subject: [PATCH 1/5] Add fix version sync handler --- .../service/jira/client/JiraRestClient.java | 17 +++++ .../jira/client/JiraRestClientBuilder.java | 21 ++++++ .../jira/handler/JiraEventHandler.java | 8 +++ .../JiraVersionUpsertEventHandler.java | 69 +++++++++++++++++++ .../jira/model/hook/JiraWebHookEvent.java | 3 +- .../jira/model/hook/JiraWebhookEventType.java | 23 +++++++ .../service/jira/model/rest/JiraVersion.java | 22 ++++++ .../replicate/jira/handler/IssueTest.java | 5 +- .../jira/mock/SampleJiraRestClient.java | 22 ++++++ 9 files changed, 187 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraVersionUpsertEventHandler.java create mode 100644 src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraVersion.java diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/client/JiraRestClient.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/client/JiraRestClient.java index 336daae..348ab6c 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/client/JiraRestClient.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/client/JiraRestClient.java @@ -16,6 +16,7 @@ import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraSimpleObject; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraTransition; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraUser; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraVersion; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; @@ -148,6 +149,22 @@ JiraIssues find(@QueryParam("jql") String query, @QueryParam("startAt") int star @Path("/issue/{issueKey}/archive") void archive(@PathParam("issueKey") String issueKey); + @GET + @Path("/version/{id}") + JiraVersion version(@PathParam("id") Long id); + + @GET + @Path("/project/{projectKey}/versions") + List versions(@PathParam("projectKey") String projectKey); + + @POST + @Path("/version") + JiraVersion create(JiraVersion version); + + @PUT + @Path("/version/{id}") + JiraVersion update(@PathParam("id") String id, JiraVersion version); + @ClientObjectMapper static ObjectMapper objectMapper(ObjectMapper defaultObjectMapper) { return defaultObjectMapper.copy().setDefaultPropertyInclusion(JsonInclude.Include.NON_EMPTY); diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/client/JiraRestClientBuilder.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/client/JiraRestClientBuilder.java index a6d0145..1fd871d 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/client/JiraRestClientBuilder.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/client/JiraRestClientBuilder.java @@ -22,6 +22,7 @@ import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraSimpleObject; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraTransition; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraUser; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraVersion; import org.jboss.resteasy.reactive.RestResponse; import org.jboss.resteasy.reactive.client.api.ClientLogger; @@ -254,6 +255,26 @@ public void archive(String issueKey) { withRetry(() -> delegate.archive(issueKey)); } + @Override + public JiraVersion version(Long id) { + return withRetry(() -> delegate.version(id)); + } + + @Override + public List versions(String projectKey) { + return withRetry(() -> delegate.versions(projectKey)); + } + + @Override + public JiraVersion create(JiraVersion version) { + return withRetry(() -> delegate.create(version)); + } + + @Override + public JiraVersion update(String id, JiraVersion version) { + return withRetry(() -> delegate.update(id, version)); + } + private static final int RETRIES = 5; private static final Duration WAIT_BETWEEN_RETRIES = Duration.of(2, ChronoUnit.SECONDS); diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraEventHandler.java index 5501a7f..f18209c 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraEventHandler.java @@ -14,6 +14,7 @@ import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraSimpleObject; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraTextContent; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraUser; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraVersion; import org.hibernate.infra.replicate.jira.service.reporting.FailureCollector; import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig; @@ -47,6 +48,13 @@ protected static URI createJiraCommentUri(JiraIssue issue, JiraComment comment) .queryParam("focusedCommentId", comment.id).build(); } + protected static URI createJiraVersionUri(JiraVersion version) { + // e.g. + // https://hibernate.atlassian.net/projects/HSEARCH/versions/32220 + return UriBuilder.fromUri(version.self).replacePath("projects").path(version.projectId).path("versions") + .path(version.id).replaceQuery("").build(); + } + protected static URI createJiraUserUri(URI someJiraUri, JiraUser user) { // e.g. // https://hibernate.atlassian.net/jira/people/557058:18cf44bb-bc9b-4e8d-b1b7-882969ddc8e5 diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraVersionUpsertEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraVersionUpsertEventHandler.java new file mode 100644 index 0000000..df774bc --- /dev/null +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraVersionUpsertEventHandler.java @@ -0,0 +1,69 @@ +package org.hibernate.infra.replicate.jira.service.jira.handler; + +import java.net.URI; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; + +import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraVersion; +import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig; + +public class JiraVersionUpsertEventHandler extends JiraEventHandler { + + public JiraVersionUpsertEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, Long id) { + super(reportingConfig, context, id); + } + + @Override + protected void doRun() { + JiraVersion version = context.sourceJiraClient().version(objectId); + List downstreamVersions = context.destinationJiraClient().versions(context.project().projectKey()); + + JiraVersion send = new JiraVersion(); + send.name = version.name; + send.description = prepareVersionDescription(version); + send.projectId = context.project().projectId(); + + Optional found = findVersion(version.id, downstreamVersions); + if (found.isPresent()) { + context.destinationJiraClient().update(found.get().id, send); + } else { + context.destinationJiraClient().create(send); + } + } + + private String prepareVersionDescription(JiraVersion version) { + URI verisonUri = createJiraVersionUri(version); + String content = """ + {quote}This [version|%s] was created as a copy of %s{quote} + + + %s""".formatted(verisonUri, version.name, version.description); + return truncateContent(content); + } + + protected Optional findVersion(String versionId, List versions) { + if (versions == null || versions.isEmpty()) { + return Optional.empty(); + } + for (JiraVersion check : versions) { + if (hasRequiredVersionQuote(check.description, versionId)) { + return Optional.of(check); + } + } + return Optional.empty(); + } + + private boolean hasRequiredVersionQuote(String body, String versionId) { + // e.g. https://hibernate.atlassian.net/projects/HSEARCH/versions/32220/ + + return Pattern.compile("(?s)^\\{quote\\}This \\[version.+/versions/%s\\].*".formatted(versionId)).matcher(body) + .matches(); + } + + @Override + public String toString() { + return "JiraVersionUpsertEventHandler{" + "objectId=" + objectId + '}'; + } +} diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/hook/JiraWebHookEvent.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/hook/JiraWebHookEvent.java index 86df3e6..5df55a7 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/hook/JiraWebHookEvent.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/hook/JiraWebHookEvent.java @@ -19,6 +19,7 @@ public class JiraWebHookEvent extends JiraBaseObject { public JiraWebHookObject comment; public JiraWebHookIssue issue; public JiraWebHookIssueLink issueLink; + public JiraWebHookObject version; public Optional eventType() { return JiraWebhookEventType.of(webhookEvent); @@ -27,6 +28,6 @@ public Optional eventType() { @Override public String toString() { return "JiraWebHookEvent{" + "webhookEvent='" + webhookEvent + '\'' + ", comment=" + comment + ", issue=" - + issue + ", otherProperties=" + properties() + '}'; + + issue + ", otherProperties=" + properties() + ", version=" + version + '}'; } } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/hook/JiraWebhookEventType.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/hook/JiraWebhookEventType.java index e44aeda..0d7182a 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/hook/JiraWebhookEventType.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/hook/JiraWebhookEventType.java @@ -12,6 +12,7 @@ import org.hibernate.infra.replicate.jira.service.jira.handler.JiraIssueLinkDeleteEventHandler; import org.hibernate.infra.replicate.jira.service.jira.handler.JiraIssueLinkUpsertEventHandler; import org.hibernate.infra.replicate.jira.service.jira.handler.JiraIssueUpsertEventHandler; +import org.hibernate.infra.replicate.jira.service.jira.handler.JiraVersionUpsertEventHandler; import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig; public enum JiraWebhookEventType { @@ -119,6 +120,28 @@ public Collection handlers(ReportingConfig reportingConfig, JiraWebHoo return List .of(new JiraCommentDeleteEventHandler(reportingConfig, context, event.comment.id, event.issue.id)); } + }, + VERSION_CREATED("jira:version_created") { + @Override + public Collection handlers(ReportingConfig reportingConfig, JiraWebHookEvent event, + HandlerProjectContext context) { + if (event.version == null || event.version.id == null) { + throw new IllegalStateException( + "Trying to handle a version event but version id is null: %s".formatted(event)); + } + return List.of(new JiraVersionUpsertEventHandler(reportingConfig, context, event.version.id)); + } + }, + VERSION_UPDATED("jira:version_updated") { + @Override + public Collection handlers(ReportingConfig reportingConfig, JiraWebHookEvent event, + HandlerProjectContext context) { + if (event.version == null || event.version.id == null) { + throw new IllegalStateException( + "Trying to handle a version event but version id is null: %s".formatted(event)); + } + return List.of(new JiraVersionUpsertEventHandler(reportingConfig, context, event.version.id)); + } }; private final String name; diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraVersion.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraVersion.java new file mode 100644 index 0000000..2437255 --- /dev/null +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraVersion.java @@ -0,0 +1,22 @@ +package org.hibernate.infra.replicate.jira.service.jira.model.rest; + +import java.net.URI; + +import org.hibernate.infra.replicate.jira.service.jira.model.JiraBaseObject; + +public class JiraVersion extends JiraBaseObject { + + public URI self; + public String id; + public String name; + public String description; + public String projectId; + + public JiraVersion() { + } + + public JiraVersion(String id) { + this.id = id; + } + +} diff --git a/src/test/java/org/hibernate/infra/replicate/jira/handler/IssueTest.java b/src/test/java/org/hibernate/infra/replicate/jira/handler/IssueTest.java index c7a0752..1f88ffa 100644 --- a/src/test/java/org/hibernate/infra/replicate/jira/handler/IssueTest.java +++ b/src/test/java/org/hibernate/infra/replicate/jira/handler/IssueTest.java @@ -17,6 +17,7 @@ import org.hibernate.infra.replicate.jira.service.jira.handler.JiraIssueLinkDeleteEventHandler; import org.hibernate.infra.replicate.jira.service.jira.handler.JiraIssueLinkUpsertEventHandler; import org.hibernate.infra.replicate.jira.service.jira.handler.JiraIssueUpsertEventHandler; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssue; import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig; import org.junit.jupiter.api.AfterEach; @@ -72,7 +73,7 @@ void testUpsert() { // - the downstream issue is updated, // - web link added pointing to the issue // - transition is performed - Mockito.verify(destination, Mockito.times(1)).update(eq("JIRATEST2-1"), any()); + Mockito.verify(destination, Mockito.times(1)).update(eq("JIRATEST2-1"), any(JiraIssue.class)); Mockito.verify(destination, Mockito.times(1)).upsertRemoteLink(eq("JIRATEST2-1"), any()); Mockito.verify(destination, Mockito.times(1)).transition(eq("JIRATEST2-1"), any()); } @@ -103,7 +104,7 @@ void testRemoveNonExisting() { // - we called the source jira and it throws 404 // - destination jira is updated (title) Mockito.verify(source, Mockito.times(1)).getIssue(eq("JIRATEST1-1")); - Mockito.verify(destination, Mockito.times(1)).update(eq("JIRATEST2-1"), any()); + Mockito.verify(destination, Mockito.times(1)).update(eq("JIRATEST2-1"), any(JiraIssue.class)); } finally { source.itemCannotBeFound.set(""); } diff --git a/src/test/java/org/hibernate/infra/replicate/jira/mock/SampleJiraRestClient.java b/src/test/java/org/hibernate/infra/replicate/jira/mock/SampleJiraRestClient.java index 3bde855..da52e9a 100644 --- a/src/test/java/org/hibernate/infra/replicate/jira/mock/SampleJiraRestClient.java +++ b/src/test/java/org/hibernate/infra/replicate/jira/mock/SampleJiraRestClient.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; @@ -23,6 +24,7 @@ import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraSimpleObject; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraTransition; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraUser; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraVersion; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -193,6 +195,26 @@ public void archive(String issueKey) { // do nothing } + @Override + public JiraVersion version(Long id) { + return new JiraVersion(Objects.toString(id, null)); + } + + @Override + public List versions(String projectKey) { + return List.of(new JiraVersion("version")); + } + + @Override + public JiraVersion create(JiraVersion version) { + return version; + } + + @Override + public JiraVersion update(String id, JiraVersion version) { + return version; + } + private JiraIssueLink sampleIssueLink(Long id) { try { return objectMapper.readValue(""" From fc56da1e7c3faddcbddbf6dfc1a2ac41e46d6e74 Mon Sep 17 00:00:00 2001 From: marko-bekhta Date: Mon, 18 Nov 2024 22:29:43 +0100 Subject: [PATCH 2/5] Add fix versions as part of the issue sync --- .../jira/handler/JiraIssueAbstractEventHandler.java | 12 +++++++++++- .../jira/service/jira/model/rest/JiraFields.java | 2 +- .../replicate/jira/export/ExportProjectTest.java | 4 ++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueAbstractEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueAbstractEventHandler.java index 4ee0069..7160cc1 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueAbstractEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueAbstractEventHandler.java @@ -19,6 +19,7 @@ import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraSimpleObject; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraTransition; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraUser; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraVersion; import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig; abstract class JiraIssueAbstractEventHandler extends JiraEventHandler { @@ -163,6 +164,15 @@ protected JiraIssue issueToCreate(JiraIssue sourceIssue, JiraIssue downstreamIss .put(epicLinkDestinationLabelCustomFieldName, sourceEpicLabel)); } + if (sourceIssue.fields.fixVersions != null) { + destinationIssue.fields.fixVersions = new ArrayList<>(); + for (JiraVersion fix : sourceIssue.fields.fixVersions) { + JiraVersion version = new JiraVersion(); + version.name = fix.name; + destinationIssue.fields.fixVersions.add(version); + } + } + return destinationIssue; } @@ -175,7 +185,7 @@ private List prepareLabels(JiraIssue sourceIssue, JiraIssue downstreamIs // let's also add fix versions to the labels if (sourceIssue.fields.fixVersions != null) { - for (JiraSimpleObject fixVersion : sourceIssue.fields.fixVersions) { + for (JiraVersion fixVersion : sourceIssue.fields.fixVersions) { String fixVersionLabel = "Fix version:%s".formatted(fixVersion.name).replace(' ', '_'); labelsToSet.add(fixVersionLabel); } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraFields.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraFields.java index 3e367d2..53e77c5 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraFields.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraFields.java @@ -17,7 +17,7 @@ public class JiraFields extends JiraBaseObject { public JiraUser assignee; public JiraUser reporter; - public List fixVersions; + public List fixVersions; // NOTE: this one is for "read-only" purposes, to create links a different API // has to be used public List issuelinks; diff --git a/src/test/java/org/hibernate/infra/replicate/jira/export/ExportProjectTest.java b/src/test/java/org/hibernate/infra/replicate/jira/export/ExportProjectTest.java index 556946f..cadd86f 100644 --- a/src/test/java/org/hibernate/infra/replicate/jira/export/ExportProjectTest.java +++ b/src/test/java/org/hibernate/infra/replicate/jira/export/ExportProjectTest.java @@ -13,7 +13,7 @@ import org.hibernate.infra.replicate.jira.service.jira.client.JiraRestClientBuilder; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssue; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssues; -import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraSimpleObject; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraVersion; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -153,7 +153,7 @@ private List formatLabels(JiraIssue issue) { } if (issue.fields.fixVersions != null) { - for (JiraSimpleObject fixVersion : issue.fields.fixVersions) { + for (JiraVersion fixVersion : issue.fields.fixVersions) { labels.add("Fix version: %s".formatted(fixVersion.name).replace(' ', '_')); } } From a4ecaed207f670f6c28c6680b673a564c1805201 Mon Sep 17 00:00:00 2001 From: marko-bekhta Date: Tue, 19 Nov 2024 11:32:48 +0100 Subject: [PATCH 3/5] Use a cached map of fix versions when syncing the issues --- .../service/jira/HandlerProjectContext.java | 92 +++++++++++++++++++ .../jira/handler/JiraEventHandler.java | 8 -- .../JiraIssueAbstractEventHandler.java | 9 +- .../JiraVersionUpsertEventHandler.java | 38 +------- .../service/jira/model/rest/JiraVersion.java | 49 ++++++++++ .../jira/export/ExportProjectTest.java | 1 + 6 files changed, 149 insertions(+), 48 deletions(-) diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java index 5dbf5f6..e275f3c 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java @@ -2,10 +2,14 @@ import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; import java.util.regex.Pattern; import org.hibernate.infra.replicate.jira.JiraConfig; @@ -16,6 +20,7 @@ import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssueBulkResponse; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssues; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraUser; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraVersion; import io.quarkus.logging.Log; @@ -26,6 +31,7 @@ public final class HandlerProjectContext implements AutoCloseable { private static final int ISSUES_PER_REQUEST = 25; private static final String SYNC_ISSUE_PLACEHOLDER_SUMMARY = "Sync issue placeholder"; private final ReentrantLock lock = new ReentrantLock(); + private final ReentrantLock versionLock = new ReentrantLock(); private final String projectName; private final String projectGroupName; @@ -43,6 +49,8 @@ public final class HandlerProjectContext implements AutoCloseable { private final Pattern sourceLabelPattern; private final DateTimeFormatter formatter; + private final Map destFixVersions; + public HandlerProjectContext(String projectName, String projectGroupName, JiraRestClient sourceJiraClient, JiraRestClient destinationJiraClient, HandlerProjectGroupContext projectGroupContext, Map allProjectsContextMap) { @@ -63,6 +71,9 @@ public HandlerProjectContext(String projectName, String projectGroupName, JiraRe this.sourceLabelPattern = Pattern .compile(projectGroupContext.projectGroup().formatting().labelTemplate().formatted(".+")); this.formatter = DateTimeFormatter.ofPattern(projectGroupContext.projectGroup().formatting().timestampFormat()); + + this.destFixVersions = getAndCreateMissingCurrentFixVersions(project, projectGroupContext, sourceJiraClient, + destinationJiraClient); } public JiraConfig.JiraProject project() { @@ -233,4 +244,85 @@ public boolean isSourceLabel(String label) { public String formatTimestamp(ZonedDateTime time) { return time != null ? time.format(formatter) : ""; } + + public JiraVersion fixVersion(JiraVersion version) { + JiraVersion v = destFixVersions.get(version.name); + if (v != null) { + return v; + } + versionLock.lock(); + try { + return destFixVersions.computeIfAbsent(version.name, + name -> upsert(project, projectGroupContext, destinationJiraClient, version, List.of())); + } catch (Exception e) { + Log.errorf(e, + "Couldn't create a copy of the fix version %s, version will not be synced for a particular Jira ticket.", + version.name); + return null; + } finally { + versionLock.unlock(); + } + } + + private static Map getAndCreateMissingCurrentFixVersions(JiraConfig.JiraProject project, + HandlerProjectGroupContext projectGroupContext, JiraRestClient sourceJiraClient, + JiraRestClient destinationJiraClient) { + Map result = new HashMap<>(); + + try { + List upstreamVersions = sourceJiraClient.versions(project.originalProjectKey()); + List downstreamVersions = destinationJiraClient.versions(project.projectKey()); + + for (JiraVersion upstreamVersion : upstreamVersions) { + JiraVersion downstreamVersion = upsert(project, projectGroupContext, destinationJiraClient, + upstreamVersion, downstreamVersions); + result.put(upstreamVersion.name, downstreamVersion); + } + } catch (Exception e) { + Log.errorf(e, "Encountered a problem while building the fix version map for %s: %s", project.projectKey(), + e.getMessage()); + } + + return result; + } + + private static JiraVersion upsert(JiraConfig.JiraProject project, HandlerProjectGroupContext projectGroupContext, + JiraRestClient jiraRestClient, JiraVersion upstreamVersion, List downstreamVersions) { + Optional version = JiraVersion.findVersion(upstreamVersion.id, downstreamVersions); + JiraVersion downstreamVersion = null; + if (version.isEmpty()) { + Log.infof("Creating a new fix version for project %s: %s", project.projectKey(), upstreamVersion.name); + downstreamVersion = processJiraVersion(project, projectGroupContext, upstreamVersion, + () -> jiraRestClient.create(upstreamVersion.copyForProject(project))); + } else if (versionNeedsUpdate(upstreamVersion, version.get())) { + Log.infof("Updating a fix version for project %s: %s", project.projectKey(), upstreamVersion.name); + downstreamVersion = processJiraVersion(project, projectGroupContext, upstreamVersion, + () -> jiraRestClient.update(version.get().id, upstreamVersion.copyForProject(project))); + } else { + downstreamVersion = version.get(); + } + return downstreamVersion; + } + + private static JiraVersion processJiraVersion(JiraConfig.JiraProject project, + HandlerProjectGroupContext projectGroupContext, JiraVersion upstreamVersion, Supplier action) { + try { + projectGroupContext.startProcessingEvent(); + return action.get(); + } catch (InterruptedException e) { + Log.error("Interrupted while trying to process fix version", e); + Thread.currentThread().interrupt(); + } catch (Exception e) { + Log.errorf(e, "Ignoring fix version sync. Unable to process fix %s version for project %s: %s", + upstreamVersion.name, project.projectKey(), e.getMessage()); + } + return null; + } + + private static boolean versionNeedsUpdate(JiraVersion upstreamVersion, JiraVersion downstreamVersion) { + return !Objects.equals(upstreamVersion.name, downstreamVersion.name) + || !Objects.equals(upstreamVersion.prepareVersionDescriptionForCopy(), downstreamVersion.description) + || upstreamVersion.released != downstreamVersion.released + || !Objects.equals(upstreamVersion.releaseDate, downstreamVersion.releaseDate); + } } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraEventHandler.java index f18209c..5501a7f 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraEventHandler.java @@ -14,7 +14,6 @@ import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraSimpleObject; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraTextContent; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraUser; -import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraVersion; import org.hibernate.infra.replicate.jira.service.reporting.FailureCollector; import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig; @@ -48,13 +47,6 @@ protected static URI createJiraCommentUri(JiraIssue issue, JiraComment comment) .queryParam("focusedCommentId", comment.id).build(); } - protected static URI createJiraVersionUri(JiraVersion version) { - // e.g. - // https://hibernate.atlassian.net/projects/HSEARCH/versions/32220 - return UriBuilder.fromUri(version.self).replacePath("projects").path(version.projectId).path("versions") - .path(version.id).replaceQuery("").build(); - } - protected static URI createJiraUserUri(URI someJiraUri, JiraUser user) { // e.g. // https://hibernate.atlassian.net/jira/people/557058:18cf44bb-bc9b-4e8d-b1b7-882969ddc8e5 diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueAbstractEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueAbstractEventHandler.java index 7160cc1..0f21ea7 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueAbstractEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueAbstractEventHandler.java @@ -166,10 +166,11 @@ protected JiraIssue issueToCreate(JiraIssue sourceIssue, JiraIssue downstreamIss if (sourceIssue.fields.fixVersions != null) { destinationIssue.fields.fixVersions = new ArrayList<>(); - for (JiraVersion fix : sourceIssue.fields.fixVersions) { - JiraVersion version = new JiraVersion(); - version.name = fix.name; - destinationIssue.fields.fixVersions.add(version); + for (JiraVersion version : sourceIssue.fields.fixVersions) { + JiraVersion downstream = context.fixVersion(version); + if (downstream != null) { + destinationIssue.fields.fixVersions.add(downstream); + } } } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraVersionUpsertEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraVersionUpsertEventHandler.java index df774bc..ee9a8e4 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraVersionUpsertEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraVersionUpsertEventHandler.java @@ -1,9 +1,7 @@ package org.hibernate.infra.replicate.jira.service.jira.handler; -import java.net.URI; import java.util.List; import java.util.Optional; -import java.util.regex.Pattern; import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraVersion; @@ -20,12 +18,9 @@ protected void doRun() { JiraVersion version = context.sourceJiraClient().version(objectId); List downstreamVersions = context.destinationJiraClient().versions(context.project().projectKey()); - JiraVersion send = new JiraVersion(); - send.name = version.name; - send.description = prepareVersionDescription(version); - send.projectId = context.project().projectId(); + JiraVersion send = version.copyForProject(context.project()); - Optional found = findVersion(version.id, downstreamVersions); + Optional found = JiraVersion.findVersion(version.id, downstreamVersions); if (found.isPresent()) { context.destinationJiraClient().update(found.get().id, send); } else { @@ -33,35 +28,6 @@ protected void doRun() { } } - private String prepareVersionDescription(JiraVersion version) { - URI verisonUri = createJiraVersionUri(version); - String content = """ - {quote}This [version|%s] was created as a copy of %s{quote} - - - %s""".formatted(verisonUri, version.name, version.description); - return truncateContent(content); - } - - protected Optional findVersion(String versionId, List versions) { - if (versions == null || versions.isEmpty()) { - return Optional.empty(); - } - for (JiraVersion check : versions) { - if (hasRequiredVersionQuote(check.description, versionId)) { - return Optional.of(check); - } - } - return Optional.empty(); - } - - private boolean hasRequiredVersionQuote(String body, String versionId) { - // e.g. https://hibernate.atlassian.net/projects/HSEARCH/versions/32220/ - - return Pattern.compile("(?s)^\\{quote\\}This \\[version.+/versions/%s\\].*".formatted(versionId)).matcher(body) - .matches(); - } - @Override public String toString() { return "JiraVersionUpsertEventHandler{" + "objectId=" + objectId + '}'; diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraVersion.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraVersion.java index 2437255..1b0d71c 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraVersion.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraVersion.java @@ -1,9 +1,16 @@ package org.hibernate.infra.replicate.jira.service.jira.model.rest; import java.net.URI; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Pattern; +import org.hibernate.infra.replicate.jira.JiraConfig; import org.hibernate.infra.replicate.jira.service.jira.model.JiraBaseObject; +import jakarta.ws.rs.core.UriBuilder; + public class JiraVersion extends JiraBaseObject { public URI self; @@ -11,6 +18,8 @@ public class JiraVersion extends JiraBaseObject { public String name; public String description; public String projectId; + public String releaseDate; + public boolean released; public JiraVersion() { } @@ -19,4 +28,44 @@ public JiraVersion(String id) { this.id = id; } + public JiraVersion copyForProject(JiraConfig.JiraProject project) { + JiraVersion copy = new JiraVersion(); + copy.name = name; + copy.description = prepareVersionDescriptionForCopy(); + copy.projectId = project.projectId(); + copy.releaseDate = releaseDate; + copy.released = released; + return copy; + } + + public String prepareVersionDescriptionForCopy() { + URI verisonUri = createJiraVersionUri(this); + return """ + {quote}This [version|%s] was created as a copy of %s{quote} + + + %s""".formatted(verisonUri, this.name, Objects.toString(this.description, "")).trim(); + } + + public static Optional findVersion(String versionId, List versions) { + if (versions == null || versions.isEmpty()) { + return Optional.empty(); + } + // e.g. https://hibernate.atlassian.net/projects/HSEARCH/versions/32220/ + Pattern pattern = Pattern.compile("(?s)^\\{quote\\}This \\[version.+/versions/%s\\].*".formatted(versionId)); + for (JiraVersion check : versions) { + if (pattern.matcher(check.description).matches()) { + return Optional.of(check); + } + } + return Optional.empty(); + } + + private static URI createJiraVersionUri(JiraVersion version) { + // e.g. + // https://hibernate.atlassian.net/projects/HSEARCH/versions/32220 + return UriBuilder.fromUri(version.self).replacePath("projects").path(version.projectId).path("versions") + .path(version.id).replaceQuery("").build(); + } + } diff --git a/src/test/java/org/hibernate/infra/replicate/jira/export/ExportProjectTest.java b/src/test/java/org/hibernate/infra/replicate/jira/export/ExportProjectTest.java index cadd86f..502e50d 100644 --- a/src/test/java/org/hibernate/infra/replicate/jira/export/ExportProjectTest.java +++ b/src/test/java/org/hibernate/infra/replicate/jira/export/ExportProjectTest.java @@ -27,6 +27,7 @@ @TestProfile(ExportProjectTest.Profile.class) @QuarkusTest +@Disabled class ExportProjectTest { private static final String DEFAULT_REPORTER_NAME = "hibernate-admins@redhat.com"; From b6c57cc2607e0fd5eb09aacb766bba18ef3e03eb Mon Sep 17 00:00:00 2001 From: marko-bekhta Date: Tue, 19 Nov 2024 11:37:13 +0100 Subject: [PATCH 4/5] Add a management endpoint to refresh fix versions if needed... --- .../jira/service/jira/HandlerProjectContext.java | 11 +++++++++++ .../replicate/jira/service/jira/JiraService.java | 12 ++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java index e275f3c..a8bca02 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java @@ -264,6 +264,17 @@ public JiraVersion fixVersion(JiraVersion version) { } } + public void refreshFixVersions() { + versionLock.lock(); + try { + destFixVersions.clear(); + destFixVersions.putAll(getAndCreateMissingCurrentFixVersions(project, projectGroupContext, sourceJiraClient, + destinationJiraClient)); + } finally { + versionLock.unlock(); + } + } + private static Map getAndCreateMissingCurrentFixVersions(JiraConfig.JiraProject project, HandlerProjectGroupContext projectGroupContext, JiraRestClient sourceJiraClient, JiraRestClient destinationJiraClient) { diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/JiraService.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/JiraService.java index 2a0c92a..bd586cf 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/JiraService.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/JiraService.java @@ -256,6 +256,18 @@ public void registerManagementRoutes(@Observes ManagementInterface mi) { triggerCommentSyncEvents(project, null, comments); rc.end(); }); + mi.router().get("/sync/fix-versions/:project").blockingHandler(rc -> { + String project = rc.pathParam("project"); + + HandlerProjectContext context = contextPerProject.get(project); + + if (context == null) { + throw new IllegalArgumentException("Unknown project '%s'".formatted(project)); + } + + context.submitTask(context::refreshFixVersions); + rc.end(); + }); } /** From d58b26147513462defbbd6941d2e8aac5b907e08 Mon Sep 17 00:00:00 2001 From: marko-bekhta Date: Tue, 19 Nov 2024 14:01:53 +0100 Subject: [PATCH 5/5] Adjust version upsert handling --- .../service/jira/HandlerProjectContext.java | 21 ++++++++++++++----- .../JiraVersionUpsertEventHandler.java | 14 +------------ 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java index a8bca02..9fb346a 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java @@ -246,14 +246,25 @@ public String formatTimestamp(ZonedDateTime time) { } public JiraVersion fixVersion(JiraVersion version) { - JiraVersion v = destFixVersions.get(version.name); - if (v != null) { - return v; + return fixVersion(version, false); + } + + public JiraVersion fixVersion(JiraVersion version, boolean force) { + if (!force) { + JiraVersion v = destFixVersions.get(version.name); + if (v != null) { + return v; + } } versionLock.lock(); try { - return destFixVersions.computeIfAbsent(version.name, - name -> upsert(project, projectGroupContext, destinationJiraClient, version, List.of())); + if (force) { + return destFixVersions.compute(version.name, (name, current) -> upsert(project, projectGroupContext, + destinationJiraClient, version, List.of())); + } else { + return destFixVersions.computeIfAbsent(version.name, + name -> upsert(project, projectGroupContext, destinationJiraClient, version, List.of())); + } } catch (Exception e) { Log.errorf(e, "Couldn't create a copy of the fix version %s, version will not be synced for a particular Jira ticket.", diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraVersionUpsertEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraVersionUpsertEventHandler.java index ee9a8e4..4a4889c 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraVersionUpsertEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraVersionUpsertEventHandler.java @@ -1,8 +1,5 @@ package org.hibernate.infra.replicate.jira.service.jira.handler; -import java.util.List; -import java.util.Optional; - import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraVersion; import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig; @@ -16,16 +13,7 @@ public JiraVersionUpsertEventHandler(ReportingConfig reportingConfig, HandlerPro @Override protected void doRun() { JiraVersion version = context.sourceJiraClient().version(objectId); - List downstreamVersions = context.destinationJiraClient().versions(context.project().projectKey()); - - JiraVersion send = version.copyForProject(context.project()); - - Optional found = JiraVersion.findVersion(version.id, downstreamVersions); - if (found.isPresent()) { - context.destinationJiraClient().update(found.get().id, send); - } else { - context.destinationJiraClient().create(send); - } + context.fixVersion(version, true); } @Override